<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Blockly Demo: Keyboard Navigation</title>
  <script src="../../blockly_compressed.js"></script>
  <script src="../../blocks_compressed.js"></script>
  <script src="../../javascript_compressed.js"></script>
  <script src="../../msg/js/en.js"></script>
  <script src="line_cursor.js"></script>
  <style>
    body {
      background-color: #fff;
      font-family: sans-serif;
    }

    h1 {
      font-weight: normal;
      font-size: 140%;
    }
    .wrapper {
      display: flex;
    }
    #keyboard_nav {
      background-color: #ededed;
      border: 1px solid black;
      padding: 1em;
    }
    #keyboard_announce {
      font-size: 1.5em;
      font-weight: 500;
      text-align: center;
    }
    #keyboard_mappings {
      font-size: 1.3em;
      font-weight: 400;
    }
    label {
      margin-right: .5em;
      min-width: 100px;
    }
    div[data-actionname] {
      display: flex;
      width: 100%;
    }
    select {
      font-size: .8em;
    }
  </style>
</head>
<body>
  <h1><a href="https://developers.google.com/blockly/">Blockly</a> &gt;
    <a href="../index.html">Demos</a> &gt; Keyboard Navigation</h1>

  <p>Keyboard Navigation is our first step towards an accessible Blockly.<br />
  For more information on how the default keyboard navigation works please see
  the <a href="https://developers.google.com/blockly/guides/configure/web/keyboard-nav">documentation</a>.
  <br />
  <br />
  <b>Pre Order Traversal</b><br />
  Feel free to just play around in accessibility mode or hit the button below to see the demo.
  The demo uses <a href="https://en.wikipedia.org/wiki/Tree_traversal#Pre-order_(NLR)">preorder tree traversal</a>
  as an alternative way to navigate the blocks,
  connections, and fields on the workspace.<br /><br />

  <b>Cursors</b><br />
  The cursor controls how the user navigates the blocks, inputs, fields and connections on a workspace.
  This demo shows three different cursors:<br />
  <b>Default Cursor:</b> Explained in <a href="https://developers.google.com/blockly/guides/configure/web/keyboard-nav">documentation</a>.<br />
  <b>Basic Cursor:</b> Uses pre order traversal to allow users to navigate
  through everything using only the previous and next command.<br />
  <b>Line Cursor:</b> We tried to make this cursor mimic a text editor. Navigating
  up and down will take the cursor to the next and previous "line" of code.
  Navigating in and out will move the cursor through all the fields and inputs
  in that "line" of code.
  </p>

  <p>
    <label for="accessibilityModeCheck">Enable Accessibility Mode:</label>
    <input type="checkbox" onclick="toggleAccessibilityMode(this.checked)" id="accessibilityModeCheck">
    <select id="cursorChanger" name="cursor" onchange="changeCursor(this.value)">
      <option value="default">Default Cursor</option>
      <option value="basic">Basic Cursor</option>
      <option value="line">Line Cursor</option>
    </select>

    <button onclick="preOrderDemo()">Start Pre-order Demo</button>
    <button onclick="stopDemo()">Stop Pre-order Demo</button>
    <label for="displayKeyMappings">Open Key Mappings:</label>
    <input type="checkbox" onclick="toggleDisplayKeyMappings(this.checked)" id="displayKeyMappings">
  </p>

  <div class="wrapper">
    <div id="blocklyDiv" style="height: 480px; width: 600px;"></div>
    <div id="keyboard_nav" style="display:none">
      <p id="keyboard_announce" aria-live="assertive">Set key mappings below</p>
      <form id="keyboard_mappings"></form>
    </div>    
  </div>

  <xml xmlns="https://developers.google.com/blockly/xml" id="toolbox" style="display: none">
    <category name="Logic" colour="%{BKY_LOGIC_HUE}">
      <block type="controls_if"></block>
      <block type="logic_compare"></block>
      <block type="logic_operation"></block>
      <block type="logic_negate"></block>
      <block type="logic_boolean"></block>
    </category>
    <category name="Loops" colour="%{BKY_LOOPS_HUE}">
      <block type="controls_repeat_ext">
        <value name="TIMES">
          <block type="math_number">
            <field name="NUM">10</field>
          </block>
        </value>
      </block>
      <block type="controls_whileUntil"></block>
    </category>
    <category name="Math" colour="%{BKY_MATH_HUE}">
      <block type="math_number">
        <field name="NUM">123</field>
      </block>
      <block type="math_arithmetic"></block>
      <block type="math_single"></block>
    </category>
    <category name="Text" colour="%{BKY_TEXTS_HUE}">
      <block type="text"></block>
      <block type="text_length"></block>
      <block type="text_print"></block>
    </category>
  </xml>

  <xml xmlns="https://developers.google.com/blockly/xml" id="startBlocks" style="display: none">
    <variables>
      <variable id="~GNXm@Z(wclI]t3zTf.g">list</variable>
      <variable id="8]s[S+Gy+%k7HoFup])m">item</variable>
    </variables>
    <block type="controls_if" x="37" y="162">
      <value name="IF0">
        <block type="logic_compare">
          <field name="OP">EQ</field>
          <value name="A">
            <block type="math_arithmetic">
              <field name="OP">ADD</field>
              <value name="A">
                <shadow type="math_number">
                  <field name="NUM">1</field>
                </shadow>
              </value>
              <value name="B">
                <shadow type="math_number">
                  <field name="NUM">1</field>
                </shadow>
              </value>
            </block>
          </value>
          <value name="B">
            <block type="math_single">
              <field name="OP">ROOT</field>
              <value name="NUM">
                <shadow type="math_number">
                  <field name="NUM">9</field>
                </shadow>
                <block type="math_number">
                  <field name="NUM">123</field>
                </block>
              </value>
            </block>
          </value>
        </block>
      </value>
      <statement name="DO0">
        <block type="lists_setIndex">
          <mutation at="true"></mutation>
          <field name="MODE">SET</field>
          <field name="WHERE">FROM_START</field>
          <value name="LIST">
            <block type="variables_get">
              <field name="VAR" id="~GNXm@Z(wclI]t3zTf.g">list</field>
            </block>
          </value>
          <next>
            <block type="text_append">
              <field name="VAR" id="8]s[S+Gy+%k7HoFup])m">item</field>
              <value name="TEXT">
                <shadow type="text">
                  <field name="TEXT"></field>
                </shadow>
              </value>
            </block>
          </next>
        </block>
      </statement>
      <next>
        <block type="controls_repeat_ext">
          <value name="TIMES">
            <shadow type="math_number">
              <field name="NUM">10</field>
            </shadow>
          </value>
        </block>
      </next>
    </block>
  </xml>

  <script>
    var demoWorkspace = Blockly.inject('blocklyDiv',
        {media: '../../media/',
         toolbox: document.getElementById('toolbox')});
    Blockly.Xml.domToWorkspace(document.getElementById('startBlocks'),
                               demoWorkspace);
    var timeout;

    var actions = [
      Blockly.navigation.ACTION_PREVIOUS,
      Blockly.navigation.ACTION_OUT,
      Blockly.navigation.ACTION_NEXT,
      Blockly.navigation.ACTION_IN,
      Blockly.navigation.ACTION_INSERT,
      Blockly.navigation.ACTION_MARK,
      Blockly.navigation.ACTION_DISCONNECT,
      Blockly.navigation.ACTION_TOOLBOX,
      Blockly.navigation.ACTION_EXIT,
      Blockly.navigation.ACTION_MOVE_WS_CURSOR_UP,
      Blockly.navigation.ACTION_MOVE_WS_CURSOR_LEFT,
      Blockly.navigation.ACTION_MOVE_WS_CURSOR_DOWN,
      Blockly.navigation.ACTION_MOVE_WS_CURSOR_RIGHT
    ];
    createKeyMappingList(actions);

    /**
     * Shows the next node in the tree traversal every second.
     * @package
     */
    function demo() {
      var doNext = function() {
        var markerManager = Blockly.getMainWorkspace().getMarkerManager();
        var nextNode = markerManager.getCursor().next();
        if (nextNode) {
          timeout = setTimeout(doNext, 1000);
        }
      }
      doNext();
    }

    /**
     * Stop the running demo.
     * @package
     */
    function stopDemo() {
      clearTimeout(timeout);
      document.getElementById('accessibilityModeCheck').disabled = false;
    };

    /**
     * Sets up accessibility mode and change the cursor to basic cursor so that
     * the demo can successfully run.
     * @package
     */
    function preOrderDemo() {
      changeCursor('basic');
      document.getElementById('accessibilityModeCheck').disabled = true;
      setTimeout(demo, 1000);
    }

    /**
     * Turn on/off accessibility mode depending on the state.
     * @param {boolean} state True to turn on accessibility mode, false otherwise.
     * @package
     */
    function toggleAccessibilityMode(state) {
      if (state) {
        Blockly.navigation.enableKeyboardAccessibility();
      } else {
        Blockly.navigation.disableKeyboardAccessibility();
      }
    }

    /**
     * Change the type of the cursor and set to the location of the old cursor.
     * Changing the cursor changes how a user navigates the blocks on the workspace.
     * @param {string} cursorType The type of the cursor.
     * @package
     */
    function changeCursor(cursorType) {
      Blockly.navigation.enableKeyboardAccessibility();
      document.getElementById('accessibilityModeCheck').checked = true;
      document.getElementById('cursorChanger').value = cursorType;
      var markerManager = Blockly.getMainWorkspace().getMarkerManager();
      var oldCurNode = markerManager.getCursor().getCurNode();
      if (cursorType === "basic") {
        Blockly.ASTNode.NAVIGATE_ALL_FIELDS = false;
        markerManager.setCursor(new Blockly.BasicCursor());
      } else if (cursorType === "line") {
        Blockly.ASTNode.NAVIGATE_ALL_FIELDS = true;
        markerManager.setCursor(new Blockly.LineCursor());
      } else {
        Blockly.ASTNode.NAVIGATE_ALL_FIELDS = false;
        markerManager.setCursor(new Blockly.Cursor());
      }
      if (oldCurNode) {
        markerManager.getCursor().setCurNode(oldCurNode);
      }
      document.activeElement.blur();
    }

    // Start key mapping demo functions

    /**
     * Save the current key map in session storage.
     * @package
     */
    function saveKeyMap() {
      var currentMap = Blockly.user.keyMap.getKeyMap();
      if (sessionStorage) {
        sessionStorage.setItem('keyMap', JSON.stringify(currentMap));
      }
    }

    /**
     * Set the key map to the map from session storage.
     * @package
     */
    function restoreKeyMap() {
      var defaultMap = Blockly.user.keyMap.getKeyMap();
      var stringifiedMap = sessionStorage.getItem('keyMap');
      var restoredMap = {};
      if (sessionStorage && stringifiedMap) {
        var keyMap = JSON.parse(stringifiedMap);
        var keys = Object.keys(keyMap);
        for (var i = 0, key; key = keys[i]; i++) {
          restoredMap[key] = Object.assign(new Blockly.Action, keyMap[key]);
        }
        Blockly.user.keyMap.setKeyMap(restoredMap);
      }
    }

    /**
     * Given the three dropdowns create the serialized key that will be stored
     * in the key map.
     * @param {Array.<Element>} selectDivs The three dropdown divs that display
     *     the key combination.
     * @package
     */
    function serializeKey(selectDivs) {
      var modifiers = Blockly.utils.object.values(Blockly.user.keyMap.modifierKeys);
      var newModifiers = [];
      var newKeyCode = '';
      var keyValue = selectDivs[2].value;

      // Get the new modifiers from the first two dropdowns.
      for (var i = 0; i < 2; i++) {
        var selectDiv = selectDivs[i];
        var key = selectDiv.value;
        if (key !== 'None') {
          newModifiers.push(key);
        }
      }
      // Get the key code from the last dropdown.
      if (keyValue !== 'None') {
        if (keyValue === 'Escape') {
          newKeyCode = Blockly.utils.KeyCodes.ESC;
        } else if (keyValue === 'Enter') {
          newKeyCode = Blockly.utils.KeyCodes.ENTER;
        } else {
          newKeyCode = keyValue.toUpperCase().charCodeAt(0);
        }
      }
      return Blockly.user.keyMap.createSerializedKey(newKeyCode, newModifiers);
    }

    /**
     * Set all dropdowns for that action to none.
     * We clear dropdowns when a user chooses the same key combination for a
     * second action.
     * @param {Blockly.Action} action The action that we want to clear the
     *     dropdowns for.
     * @package
     */
    function clearDropdown(action) {
      var actionDiv = document.querySelectorAll('[data-actionname='+ action.name +']')[0];
      var selectDivs = actionDiv.getElementsByTagName('select');
      for (var i = 0, selectDiv; selectDiv = selectDivs[i]; i++) {
        selectDiv.value = 'None';
      }
    }

    /**
     * Given the three dropdowns create a human readable string so the screen reader
     * can read it out.
     * @param {Array.<Element>} selectDivs The three dropdown divs that display
     *     the key combination.
     * @package
     */
    function getReadableKey(selectDivs) {
      var readableKey = '';

      for (var i = 0, selectDiv; selectDiv = selectDivs[i]; i++) {
        if (selectDiv.value !== 'None') {
          readableKey += selectDiv.value + ' ';
        }
      }
      return readableKey;
    }

    /**
     * Update the key in the key map when the user selects a new value in one of the
     * dropdowns.
     * @param {Event} e The event dispatched from changing a dropdown.
     * @package
     */
    function updateKey(e) {
      var keyboardAnnouncerText = '';
      var actionDiv = e.srcElement.parentElement;
      var action = actionDiv.action;
      var selectDivs = actionDiv.getElementsByTagName('select');
      var key = serializeKey(selectDivs);
      var oldAction = Blockly.user.keyMap.getActionByKeyCode(key);

      if (oldAction) {
        keyboardAnnouncerText += oldAction.name + ' action key was overwritten. \n';
        clearDropdown(oldAction);
      }
      keyboardAnnouncerText += action.name + ' key was set to ' + getReadableKey(selectDivs);
      document.getElementById('keyboard_announce').innerText = keyboardAnnouncerText;
      Blockly.user.keyMap.setActionForKey(key, action);
      saveKeyMap();
      document.activeElement.blur();
    }

    /**
     * Set the key to be the correct value from the key map.
     * @param {string} actionKey The serialized key for a given action.
     * @param {Element} keyDropdown The dropdown that displays the primary key.
     * @package
     */
    function setKeyDropdown(actionKey, keyDropdown) {
      // Strip off any modifier to just get the key code.
      var keyCode = actionKey.match(/\d+/)[0];
      var keyValue = String.fromCharCode(keyCode);
      if (parseInt(keyCode) === Blockly.utils.KeyCodes.ESC) {
          keyValue = 'Escape';
      } else if (parseInt(keyCode) === Blockly.utils.KeyCodes.ENTER) {
        keyValue = 'Enter';
      }
      keyDropdown.value = keyValue;
    }

    /**
     * Set the modifiers to be the correct value from the key map.
     * @param {string} actionKey The key code holding the modifiers and key.
     * @param {Array.<Element>} modifierDropdowns A list of dropdowns for
     *     the modifier values.
     * @package
     */
    function setModifiers(actionKey, modifierDropdowns) {
      var modifiers = Blockly.utils.object.values(Blockly.user.keyMap.modifierKeys);
      for (var i = 0; i < 2; i++) {
        var modifierDropdown = modifierDropdowns[i];
        for (var j = 0, modifier; modifier = modifiers[j]; j++) {
          if (actionKey.indexOf(modifier) > -1) {
            modifierDropdown.value = modifier;
            actionKey = actionKey.replace(modifier, '');
            break;
          }
        }
      }
    }

    /**
     * Set the dropdowns to display the correct combination of modifiers and
     * keys for the action key.
     * @param {Blockly.Action} action The Blockly action.
     * @param {Element} actionDiv The div holding the dropdowns and label for the
     *     given action.
     * @param {string} actionKey The key corresponding to the given action.
     * @package
     */
    function setDropdowns(action, actionDiv, actionKey) {
      var selectDivs = actionDiv.getElementsByTagName('select');
      if (actionKey) {
        setModifiers(actionKey, selectDivs);
        setKeyDropdown(actionKey, selectDivs[selectDivs.length - 1]);
      } else {
        clearDropdown(action);
      }
    }

    /**
     * Create a dropdown with the given list of possible keys.
     * @param {Blockly.Action} action The Blockly action.
     * @param {Element} actionDiv The div holding the dropdowns and labels for
     *     a given action.
     * @param {Array.<string>} keys The list of keys to add to the dropdown.
     * @package
     */
    function createDropdown(action, actionDiv, keys) {
      var select = document.createElement('select');
      select.addEventListener('change', updateKey);
      select.setAttribute('aria-labelledby', action.name + '_label');
      for (var i = 0, key; key = keys[i]; i++) {
        select.options.add(new Option(key, key));
      }
      actionDiv.appendChild(select);
    }

    /**
     * Create two dropdowns that display possible modifiers and a single dropdown
     * displaying a list of keys.
     * @param {Blockly.Action} action The Blockly action.
     * @param {string} actionKey The key corresponding to the given action.
     * @param {Element} actionDiv The div holding the dropdowns and label for the
     *     given action.
     * @package
     */
    function createDropdowns(action, actionKey, actionDiv) {
      var modifiers = ['None'].concat(Blockly.utils.object.values(Blockly.user.keyMap.modifierKeys));
      var keys = ['None', 'Enter', 'Escape'].concat("ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890".split(''));
      createDropdown(action, actionDiv, modifiers);
      createDropdown(action, actionDiv, modifiers);
      createDropdown(action, actionDiv, keys);
      setDropdowns(action, actionDiv, actionKey);
    }

    /**
     * For each action create a row of 3 dropdowns and an action label. Update
     * the dropdowns to reflect the value in the key map.
     * @param {Array.<Blockly.Action>} actions List of blockly actions.
     * @package
     */
    function createKeyMappingList(actions) {
      // Update the key map to reflect the key map saved in session storage.
      restoreKeyMap();
      var keyMapDiv = document.getElementById('keyboard_mappings');
      for (var i = 0, action; action = actions[i]; i++) {
        var actionDiv = document.createElement('div');
        actionDiv.setAttribute('data-actionname', action.name);
        actionDiv.action = action;

        var labelDiv = document.createElement('label');
        labelDiv.innerText = action.name;
        labelDiv.setAttribute('id', action.name + '_label');

        actionDiv.appendChild(labelDiv);
        keyMapDiv.appendChild(actionDiv);

        var actionKey = Blockly.user.keyMap.getKeyByAction(action);
        createDropdowns(action, actionKey, actionDiv);
      }
    }

    /**
     * Hide/show the key map panel.
     * @param {boolean} state The state of the checkbox. True if checked, false
     *     otherwise.
     * @package
     */
    function toggleDisplayKeyMappings(state) {
      if (state) {
        document.getElementById('keyboard_nav').style.display = 'block';
      } else {
        document.getElementById('keyboard_nav').style.display = 'none';
      }
    }
    // End key mapping demo functions

  </script>

</body>
</html>
